const request = require('base')
const handleType = require('./handleType')
const tableName = 'z-ai'
/**
 * 获取大语言模型
 */
const getManager = (params) => {
	const {
		provider, // 使用uni-ai计费网关，不指定provider，默认值minimax llm服务商，目前支持openai、baidu、minimax、azure（azure新增于HBuilderX 3.8.3）、ifly（ifly新增于HBuilderX 3.8.10）、deepseek (deepseek新增于HBuilderX 4.52)、aliyun-bailian（aliyun-bailian新增于HBuilderX 4.71）
		apiKey, // llm服务商的apiKey
		endpoint, // azure服务端点，在azure创建ai服务时获取
		groupId,  // 	minimax的groupId参数。
		accessToken,  // llm服务商的accessToken。baidu必填
		proxy, // 可有效连接openai服务器的、可被uniCloud云函数连接的代理服务器地址。格式为IP或域名，域名不包含http前缀，协议层面仅支持https。配置为openai时必填。使用国外openai时必填，其他情况不填
		appId, // 客户端manifest.json内的appId，部分场景下（云函数url化、定时触发）云函数/云对象无法获取客户端appId，需要通过此参数传递
		apiSecret // llm服务商的apiSecret。
	} = params || {}
	return uniCloud.ai.getLLMManager(params)
}

/**
 * https://doc.dcloud.net.cn/uniCloud/uni-ai.html
 * 单次AI对话
 * data：对话参数
 * params：获取大语言模型的参数
 * 
 * prompt在前端一般有可能来自三个地方：
 * 1，用户输入的prompt，用户可修改。一般位置在最后
 * 2，用户输入场景中预设的prompt，在前端设定，用户不可见。一般与第一种情况合并，组成同一个prompt，但是位置在前面
 * 3，系统对用户场景的设定。在后端设置，云函数中修改，用户不可见。
 */
module.exports = async (data, params) => {
	var {
		type, // 额外的对话类型。通过该值会增加messages数组内的内容。比如：title、summary等，该值也可以时i对象包含type、mode等细分值
		mode = 'custom', // AI对话模式。qa：问答；reasoning：推理；article：文章生成；code：代码生成；translation：翻译；custom：自定义。默认值qa
		acticle_code, // 文章code
		doc, // 上个对话id，如果存在就是继续对话，需查询该对话树的所有信息拼接messages，如果不存在，表示新对话，
		system, // 系统设定，即预置信息。比如需要其专业性的回答时，设置system。system角色及信息如存在时只能放在messages数组第一项。ifly、baidu不支持此角色
		prompt, // 用户输入信息。可以是数组，表示多条
		wordNum, // 生成文本的长度上限
		style, // 语言风格
		/**
		 * array<object> 提问消息。优先级低于doc、system、prompt，建议使用prompt模式！使用prompt模式会自动捕捉标题
		 * 	role：角色，有三个值：system、user、assistant，分别代表系统、用户、助手
		 * 		system：系统设定，即预置信息。比如需要其专业性的回答时，设置system。system角色及信息如存在时只能放在messages数组第一项。ifly、baidu不支持此角色
		 * 		user：用户信息，即用户输入的信息。比如需要其回答问题，设置user。user角色及信息如存在时只能放在messages数组第一项。ifly、baidu不支持此角色
		 * 		assistant：ai助手，对应的content为ai返回的信息。assistant这个角色的内容，是ai返回的。当需要持续聊天、记忆前文时，需使用此角色。
		 * 	content：即消息内容，字符串类型
		 * 
		 * 对于不支持system的情况，如baidu，只能把system对应的内容写到第一条user信息内，也可以达到一定范围内的控制效果。
		 * 注意：对于法律、医学等专业领域需要准确回答的，建议使用gpt-4模型，可使用更专业的role: 'system。其他模型更适合闲聊、文章内容生成。
		 */
		messages = [{
			role: 'system',
			content: '你是一名律师，回答内容仅限法律范围。以下对话只需给出结果，不要对结果进行解释。'
		}, {
			role: 'user',
			content: '谣言传播多少人可以定罪？'
		}, {
			role: "assistant",
			content: "2020 年 2 月中国法律规定，利用突发传染病疫情等灾害，制造、传播谣言构成犯罪的，依法追究刑事责任。但该法并未将具体“人数”作为入罪门槛。 \n\n澳大利亚维州法律规定，你可以因为传播虚假信息被起诉。 2019 年，维州最高法院的 Janice MacDonald 法官曾以 525 人的死亡人数（摔伤事故致死）为例提出参考要求。这可能导致虚假或误导性的信息传播较广且具有中等负面影响。\n\n所以，没有具体的“人数”作为传播谣言是否会构成犯罪的依据，这取决于各国的法律法规以及法律法规解释的不同。",
		}, {
			role: 'user',
			content: '如何定义谣言？或者普通人如何才能判定那是谣言呢？'
		}],
		/**
		 * 模型名称。每个AI Provider有多个model
		 * openai：gpt-4、gpt-4-0314、gpt-4-32k、gpt-4-32k-0314、gpt-3.5-turbo（默认值）、gpt-3.5-turbo-0301
		 * minimax：abab4-chat、abab5-chat（默认值）、abab6.5s-chat
		 * azure（通过uni-ai计费网关调用）：gpt-3.5-turbo（默认值）
		 * baidu（文心千帆）：ERNIE-Bot（默认值）、ERNIE-Bot-turbo
		 * ifly：lite（默认值）、generalv3、pro-128k、generalv3.5、max-32k、4.0Ultra
		 * deepseek：deepseek-chat（默认值）、deepseek-reasoner
		 * aliyun-bailian：支持阿里云百炼模型广场所有模型
		 * 
		 * 模型最大上下文token数、字数限制
		 * 	openai gpt-3.5-turbo: 4097
		 * 	azure gpt-3.5-turbo: 8192
		 * 	abab4-chat、abab5-chat: 4096
		 * 	deepseek-chat、deepseek-reasoner: 8192
		 * 	baidu文心千帆最后一个message的content长度（即此轮对话的问题）不能超过2000个字符；如果messages中content总长度大于2000字符，系统会依次遗忘最早的历史会话，直到content的总长度不超过2000个字符
		 * deepseek-reasoner模型是deepseek的推理模型，在正式回答之前会生成思考过程，用于推理、解释、问答等场景。deepseek-reasoner的输出 token 数包含了思维链和最终答案的所有 token。
		 */
		model,
		/**
		 * azure模型部署id，如使用uni-ai计费网关无需传递此参数、而是要传model
		 * 自行设置apikey调用azure接口时需要传deploymentId，使用uni-ai计费网关访问azure服务时需要传递model而不是deploymentId。目前通过uni-ai计费网关调用azure接口仅支持gpt-3.5-turbo这一个模型。
		 */
		deploymentId,
		/**
		 * 数值 生成的token数量限制，需要注意此值和传入的messages对应的token数量相加不可大于模型最大上下文token数
		 */
		tokensToGenerate = 4096,
		/**
		 * 较高的值将使输出更加随机，而较低的值将使输出更加集中和确定。建议temperature和top_p同时只调整其中一个
		 */
		temperature,
		/**
		 * 采样方法，数值越小结果确定性越强；数值越大，结果越随机
		 */
		topP,
		/**
		 * 是否使用流式响应
		 */
		stream,
		sseChannel,
		/**
		 * 自动处理流式响应时使用的流式响应回调事件，可选：message、optimizedMessage、line
		 * 默认message
		 */
		streamEventForSSE,
		webSearch // 启用网络搜索能力，仅abab6.5s-chat(minimax)、4.0Ultra(ifly)模型支持
	} = data || {}
	if (temperature > 1) {
		temperature = temperature * 0.01
	}
	if (topP > 1) {
		topP = topP * 0.01
	}
	// 通过prompt得到唯一的一条content
	var content = Array.isArray(prompt) ? prompt.join('\n') : prompt
	// return
	const {
		provider, // 使用uni-ai计费网关，不指定provider，默认值minimax llm服务商，目前支持openai、baidu、minimax、azure（azure新增于HBuilderX 3.8.3）、ifly（ifly新增于HBuilderX 3.8.10）、deepseek (deepseek新增于HBuilderX 4.52)、aliyun-bailian（aliyun-bailian新增于HBuilderX 4.71）
		apiKey, // llm服务商的apiKey
		endpoint, // azure服务端点，在azure创建ai服务时获取
		groupId,  // minimax的groupId参数。
		accessToken,  // llm服务商的accessToken。baidu必填
		proxy, // 可有效连接openai服务器的、可被uniCloud云函数连接的代理服务器地址。格式为IP或域名，域名不包含http前缀，协议层面仅支持https。配置为openai时必填。使用国外openai时必填，其他情况不填
		appId, // 客户端manifest.json内的appId，部分场景下（云函数url化、定时触发）云函数/云对象无法获取客户端appId，需要通过此参数传递
		apiSecret // llm服务商的apiSecret。
	} = params || {}
	var title, titleSymbol // 标题与标识
	const handleTitle = () => { // 处理标题
		if (!['article', 'translation'].includes(mode)) { // 这两项不需要标题
			titleSymbol = ['《标题：', '》']
			const laststr = `回答完毕后总结本次问答的主题，在答复的最前面插入“${titleSymbol[0]}{主题}${titleSymbol[1]}”。如果有字数限制，该插入的内容不在限定的字数内。`
			return {
				role: 'user',
				content: laststr
			}
		}
		return undefined
	}
	const analyseTitle = (reply) => { // 解析标题
		if (titleSymbol && reply.includes(titleSymbol[0]) && reply.includes(titleSymbol[1])) {
			const arr = reply.split(titleSymbol[1])
			if (arr.length > 1) {
				if (arr[0].startsWith(titleSymbol[0])) { // 满足既定的标题格式
					title = arr[0].substr(titleSymbol[0].length - arr[0].length)
					// 第一项是标题，则去掉
					reply = arr.slice(1).join(titleSymbol[1])
					// 去掉开头的\n。以下.replace(/^\s+/, '')等同于.trimStart()，会替换所有的空白字符（包括换行符 \n、回车符 \r、制表符 \t 等）
					// reply = reply.replace(/^\s+/, '')
					// 若只需移除换行符（\n 和 \r），可使用 /^[\n\r]+/
					reply = reply.replace(/^[\n\r]+/, '')
					if (!reply) {
						reply = title
					}
				}
			}
			if (!title) { // 没有标题，则取提问内容的第一个断句符号前面的内容
				title = content.split(/[，。？！；,.?!;\n\r\t]/)[0]
				if (title.length > 24) { // 并且不超过24个字符
					title = title.substr(0, 24)
				}
			}
		}
		return reply
	}
	if (content) { // content存在，messages无效，要重新拼接
		messages = [] // 清空messages
		if (doc) { // 查询对话树
			const limitLevel = 10 // 最大查询层级（不包含当前层级），可以省略默认10级，最大15，最小1。如果超过15项则要继续查询
			const dataList = []
			try {
				var res = await request.get({name: tableName, where: {
					field: 'content,reply,parent_id',
					parentKey: 'parent_id',
					getTreePath: {
						limitLevel: 'all', // 最大查询层级（不包含当前层级），可以省略默认10级，最大15，最小1。如果超过15项则要继续查询
						startWith: `_id=="${doc}"`,  // 末级节点的条件，此初始条件不可以省略
						isNew: true
					}
				}})
				res.data.forEach(item => {
					if (item.system && !system) { // 记录预置信息
						system = item.system
					}
					if (item.content) { // 添加之前的问题
						messages.push({
							role: 'user',
							content: item.content
						})
					}
					if (item.reply) { // 添加问题的回复
						messages.push({
							role: 'assistant',
							content: item.reply,
						})
					}
				})
			} catch (error) {
				return {
					errSubject: 'ai-llm',
					errCode: 'param-error',
					errMsg: error.message || error
				}
			}
		}
		if (system) { // 添加到第一项
			messages.unshift({
				role: 'system',
				content: system
			})
		}
		if (!doc) { // 添加包含本次问答标题的提示语
			var obj = handleTitle()
			if (obj) {
				messages.push(obj)
			}
		}
		messages.push({ // 添加到最后一项
			role: 'user',
			content
		})
	} else {
		try {
			if (messages === undefined) {
				throw "messages为必传参数"
			} else if(!Array.isArray(messages)) {
				throw "参数messages的值类型必须是数组"
			} else {
				messages.forEach(item=>{
					if (typeof item != 'object') {
						throw "参数messages的值类型必须是对象数组"
					}
					let itemRoleArr = ["assistant","user","system"]
					if (!itemRoleArr.includes(item.role)) {
						throw "参数messages[{role}]的值只能是：" + itemRoleArr.join('或')
					}
					if (typeof item.content != 'string') {
						throw "参数messages[{content}]的值类型必须是字符串"
					}
				})
			}
		} catch (errMsg) {
			return {
				errSubject: 'ai-llm',
				errCode: 'param-error',
				errMsg
			}
		}
		var obj = handleTitle()
		if (messages.length > 1) {
			if (!system) {
				messages.every(item => { // 查询第一个非assistant的内容作为system
					if (item.role === 'system' || item.role === 'user') {
						system = item.content
						return false
					}
					return true
				})
			}
			// 查询最后一项的user内容作为content
			for (let i = messages.length - 1; i >= 0; i--) {
				if (messages[i].role === 'user') {
					content = messages[i].content
					if (obj) {
						messages.splice(i - 1, 0, obj)
					}
					break
				}
			}
		} else {
			content = messages[0].content
			if (obj) {
				messages.push(obj)
			}
		}
	}
	handleType(type, messages, content)
	var laststr = ''
	if (style) {
		if (!Array.isArray(style)) {
			style = [style]
		}
		if (style.length > 0) {
			laststr += `回答风格为${style.join('、')}；`
		}
	}
	if (wordNum) {
		laststr += `回答字数不超过${wordNum}。`
	}
	if (laststr) {
		messages.push({
			role: 'user',
			content: laststr
		})
	}
	return new Promise((resolve, reject) => {
		// 获取大语言模型
		const llmManager = getManager(params)
		// 对话
		llmManager.chatCompletion({
			messages, model, deploymentId, tokensToGenerate, temperature, topP, stream, sseChannel, streamEventForSSE, webSearch
		}).then(res => {
			var {
				id, // 本次回复的id
				reply,  // ai对本次消息的回复
				/**
				 * array<object> 启用网络搜索后本次消息中出现的内容引用注解
				 * text: 注解名称；用于匹配消息中的注解占位字符串
				 * url: 引用内容地址
				 * quote: 引用内容摘要
				 */
				annotations,
				/**
				 * 本次对话token消耗详情
				 * promptTokens：输入的token数量
				 * completionTokens：生成的token数量
				 * totalTokens：总token数量
				 * reasoningTokens：仅适用于 deepseek-reasoner 模型。推理模型所产生的思维链 token 数量
				 */
				usage = {totalTokens: 191},
				/**
				 * array<object> 所有生成结果
				 * finishReason: 截断原因，stop（正常结束）、length（超出maxTokens被截断）
				 * message：对象。返回消息
				 * 	role: 角色
				 * 	content: 对应角色消息内容，字符串类型
				 * reasoningContent：仅适用于 deepseek-reasoner 模型。内容为 assistant 消息中在最终答案之前的推理内容
				 * annotations：array<object> 启用网络搜索后本次消息中出现的内容引用注解
				 * 	text: 注解名称；用于匹配消息中的注解占位字符串
				 * 	url: 引用内容地址
				 * 	quote: 引用内容摘要
				 * 
				 */
				choices, // 每个元素包含：finishReason: 'stop', message: {annotations: [], role: 'assistant', content: Uni-app是一款使用Vue.js开发所有前端应用的框架。'}
			} = res
			console.log(res)
			reply = analyseTitle(reply)
			var finishReason, reasoningContent
			if (choices && choices.length > 0) {
				finishReason = choices[0].finishReason
				reasoningContent = choices[0].reasoningContent
			}
			const dataInfo = {
				title, rid: id, aid: acticle_code, parent_id: doc, totalTokens: usage.totalTokens,
				reply, annotations, usage, finishReason, provider, model, system, content, mode,
				tokensToGenerate, temperature, topP, stream, sseChannel, streamEventForSSE, webSearch
			}
			request.add({name: tableName, data: dataInfo, isReturn: true}).then(res => {
				resolve(request.getResult({data: res.data[0], message: 'AI对话成功'}))
			}).catch(err => {
				resolve(request.getResult({data: dataInfo}))
			})
		}).catch(err => {
			console.log(err)
			reject(request.getError(err))
		})
	})
}